﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using Migrator.Exceptions;

namespace Migrator.Util
{
    /// <summary>
    /// Represents an IPv4 address
    /// </summary>
    public class IPAddress
    {
        public static IPAddress NullAddress = new IPAddress(0, 0, 0, 0);
        public static IPAddress BroadcastAddress = new IPAddress(255, 255, 255, 255);

        #region Constructors
        /// <summary>
        /// Creates a new IP address
        /// </summary>
        /// <param name="address">The string representation of the address</param>
        public IPAddress(string address)
        {
            this.octets = Parse(address);
        }

        /// <summary>
        /// Creates a new IP address
        /// </summary>
        /// <param name="byte1">The first byte of the IP address</param>
        /// <param name="byte2">The second byte of the IP address</param>
        /// <param name="byte3">The third byte of the IP address</param>
        /// <param name="byte4">The fourth byte of the IP address</param>
        public IPAddress(byte byte1, byte byte2, byte byte3, byte byte4)
        {
            this.octets = new byte[] { byte1, byte2, byte3, byte4 };
        }

        /// <summary>
        /// Creates a new IP address
        /// </summary>
        /// <param name="address">The 4 bytes of the IP address</param>
        public IPAddress(byte[] octets)
        {
            this.octets = (byte[]) octets.Clone();
        }

        #endregion

        #region Public methods

        /// <summary>
        /// Checks if this address belongs to the given network
        /// </summary>
        /// <param name="networkAddress">Network's address</param>
        /// <returns>True if the address belongs to the network, false otherwise</returns>
        public bool IsInNetwork(IPAddress networkAddress)
        {
            bool foundZero = false;
            for (int i = 0; i < 4; i++)
            {
                if (this.octets[i] != networkAddress.octets[i])
                {
                    if (networkAddress.octets[i] == 0)
                    {
                        foundZero = true;
                    }
                    else
                    {
                        return false;
                    }
                }
                else if (foundZero == true)
                {
                    return false;
                }
            }

            return true;
        }

        /// <summary>
        /// Returns the address as a byte[]
        /// </summary>
        /// <returns>An array containing the bytes of the address</returns>
        public byte[] GetAddressBytes()
        {
            return (byte[]) this.octets.Clone();
        }

        public override string ToString()
        {
            return String.Join(".", octets.Select(s => s.ToString()).ToArray());
        }

        public override bool Equals(object obj)
        {
            // Check for null and compare run-time types.
            if (obj == null || GetType() != obj.GetType()) 
                return false;

            IPAddress other = (IPAddress) obj;
            for (int i = 0; i < 4; i++)
            {
                if (this.octets[i] != other.octets[i])
                    return false;
            }

            return true;
        }

        public override int GetHashCode()
        {
            return this.octets.Aggregate(0, (accum, value) => accum ^= value);
        }

        #endregion

        #region Operators
        
        /// <summary>
        /// Implicit conversion between IPAddress and string
        /// </summary>
        /// <param name="address">The string representation of the IP address</param>
        /// <returns>The created IPAddress instance</returns>
        public static implicit operator IPAddress(string address)
        {
            return new IPAddress(address);
        }

        public static bool operator ==(IPAddress a, IPAddress b)
        {
            // If both are null, or both are same instance, return true.
            if (System.Object.ReferenceEquals(a, b))
            {
                return true;
            }

            // If one is null, but not both, return false.
            if (((object)a == null) || ((object)b == null))
            {
                return false;
            }

            // Return true if the fields match:
            return a.Equals(b);
        }

        public static bool operator !=(IPAddress a, IPAddress b)
        {
            return !(a == b);
        }

        #endregion


        #region Private methods
        /// <summary>
        /// Parses an IP address from its string representation
        /// </summary>
        /// <param name="address">The string representation of the address</param>
        /// <exception cref="InvalidIPv4AddressException">
        /// Thrown when the given address is invalid
        /// </exception>
        /// <returns>A byte array with the 4 octets</returns>
        private byte[] Parse(string address)
        {
            const string pattern = "^" +
                                         @"([01]?\d?\d|2[0-4]\d|25[0-5])\." +
                                         @"([01]?\d?\d|2[0-4]\d|25[0-5])\." +
                                         @"([01]?\d?\d|2[0-4]\d|25[0-5])\." +
                                         @"([01]?\d?\d|2[0-4]\d|25[0-5])" +
                                   "$";

            Match match = Regex.Match(address, pattern);
            if (match.Success)
            {
                byte first = Byte.Parse(match.Groups[1].Value);
                byte second = Byte.Parse(match.Groups[2].Value);
                byte third = Byte.Parse(match.Groups[3].Value);
                byte fourth = Byte.Parse(match.Groups[4].Value);

                return new byte[] { first, second, third, fourth };
            }

            throw new InvalidAddressException(address);
        }
        #endregion

        #region Private data
        protected byte[] octets; 
        #endregion
    }
}
